﻿// -----------------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
// -----------------------------------------------------------------------
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.Internal.Collections;

namespace Microsoft.Internal
{
    internal static class WeakCollection
    {
        private static List<MethodInfo> GetPublicNonGenericMethods(Type targetType)
        {
            return
                (from method in targetType.GetMethods()
                 where !method.IsGenericMethod
                 select method).ToList();
        }

        public static bool IsPromotableWeakCollection(Type targetType, out Type itemType)
        {
            itemType = null;
            if (typeof(IEnumerable).IsAssignableFrom(targetType))
            {
                MethodInfo add;
                if (TryFindAddMethod(targetType, GetPublicNonGenericMethods(targetType), out add))
                {
                    itemType = add.GetParameters()[0].ParameterType;
                    return true;
                }
            }
            return false;
        }

        private static bool TryFindAddMethod(Type type, List<MethodInfo> methods, out MethodInfo add)
        {
            var addMethods = (from method in methods
                              where
                                  method.Name == "Add" &&
                                  method.GetParameters().Length == 1 &&
                                  (method.ReturnType == typeof(void) ||
                                   method.ReturnType == typeof(int) ||
                                   method.ReturnType == typeof(bool))
                              select method).ToList();

            if (addMethods.Count == 1)
            {
                add = addMethods[0];
                return true;
            }
            else if (addMethods.Count > 1)
            {
                // disambiguate using indexer
                List<Type> indexerTypes =
                    (from property in
                         from member in type.GetDefaultMembers()
                         select member as PropertyInfo
                     where property != null
                     group property by property.PropertyType
                     into propertyType
                         select propertyType.Key).ToList();

                if (indexerTypes.Count == 1)
                {
                    // find an add that takes the same 
                    add = (from method in addMethods
                           where method.GetParameters()[0].ParameterType == indexerTypes[0]
                           select method).FirstOrDefault();
                    if (add != null)
                    {
                        return true;
                    }
                }
            }

            add = null;
            return false;
        }

        private static bool TryFindClearMethod(List<MethodInfo> methods, out MethodInfo clear)
        {
            var clearMethods = (from method in methods
                                let parameters = method.GetParameters()
                                where
                                    method.Name == "Clear" &&
                                    parameters.Length == 0 &&
                                    method.ReturnType == typeof(void)
                                select method).ToList();

            if (clearMethods.Count == 1)
            {
                clear = clearMethods[0];
                return true;
            }

            clear = null;
            return false;
        }

        private static bool TryFindRemoveMethod(List<MethodInfo> methods, Type addItemType, out MethodInfo remove)
        {
            var removeMethods = (from method in methods
                                 let parameters = method.GetParameters()
                                 let returnType = method.ReturnType
                                 where
                                     method.Name == "Remove" &&
                                     parameters.Length == 1 &&
                                     parameters[0].ParameterType == addItemType &&
                                     (returnType == typeof(void) || returnType == typeof(bool))
                                 select method).ToList();

            if (removeMethods.Count == 1)
            {
                remove = removeMethods[0];
                return true;
            }

            remove = null;
            return false;
        }

        // result will actually be ICollection<itemType> on success
        public static bool TryPromoteWeakCollection(object target, out object result, out Type itemType)
        {
            itemType = null;
            result = null;

            Type targetType = target.GetType();

            if (!(target is IEnumerable))
            {
                return false;
            }

            // cache method subset for multiple queries
            List<MethodInfo> methods = GetPublicNonGenericMethods(targetType);

            // IEnumerable+Add is the minimum bar to be a writable sequence
            MethodInfo add;
            if (!TryFindAddMethod(targetType, methods, out add))
            {
                return false;
            }
            itemType = add.GetParameters()[0].ParameterType;

            // With a remove method, we will implement the entire
            // ICollection<object> interface
            MethodInfo remove;
            TryFindRemoveMethod(methods, itemType, out remove);

            // We use the clear method, if available, as an optimization
            MethodInfo clear;
            TryFindClearMethod(methods, out clear);

            Type adapterType;
            if (target is INotifyCollectionChanged)
            {
                adapterType = typeof(WeakCollectionImplementationINCC<>).MakeGenericType(itemType);
            }
            else
            {
                adapterType = typeof(WeakCollectionImplementation<>).MakeGenericType(itemType);
            }

            result = Activator.CreateInstance(adapterType, target, add, clear, remove);
            return true;
        }

        private class WeakCollectionImplementation<T> : ICollection<T>
        {
            private IEnumerable sequence;
            private Action<T> addMethod;
            private Action clearMethod;
            private Func<T, bool> removeMethod;

            public WeakCollectionImplementation(
                IEnumerable sequence,
                MethodInfo addMethodInfo,
                MethodInfo clearMethodInfo,
                MethodInfo removeMethodInfo)
            {
                this.sequence = sequence;
                if (addMethodInfo.ReturnType == typeof(void))
                {
                    this.addMethod = (Action<T>) Delegate.CreateDelegate(typeof(Action<T>), sequence, addMethodInfo);
                }
                else if (addMethodInfo.ReturnType == typeof(bool))
                {
                    Func<T, bool> addMethodBool =
                        (Func<T, bool>) Delegate.CreateDelegate(typeof(Func<T, bool>), sequence, addMethodInfo);
                    this.addMethod = item => addMethodBool(item);
                }
                else if (addMethodInfo.ReturnType == typeof(int))
                {
                    Func<T, int> addMethodInt =
                        (Func<T, int>) Delegate.CreateDelegate(typeof(Func<T, int>), sequence, addMethodInfo);
                    this.addMethod = item => addMethodInt(item);
                }

                if (clearMethodInfo != null)
                {
                    this.clearMethod = (Action) Delegate.CreateDelegate(typeof(Action), sequence, clearMethodInfo);
                }

                if (removeMethodInfo != null)
                {
                    if (removeMethodInfo.ReturnType == typeof(bool))
                    {
                        this.removeMethod =
                            (Func<T, bool>) Delegate.CreateDelegate(typeof(Func<T, bool>), sequence, removeMethodInfo);
                    }
                    else if (removeMethodInfo.ReturnType == typeof(void))
                    {
                        Action<T> removeMethodVoid =
                            (Action<T>) Delegate.CreateDelegate(typeof(Action<T>), sequence, removeMethodInfo);
                        this.removeMethod = item =>
                        {
                            if (this.Contains(item))
                            {
                                removeMethodVoid(item);
                                return true;
                            }

                            return false;
                        };
                    }
                }
            }

            public void Add(T item)
            {
                this.addMethod(item);
            }

            public void Clear()
            {
                if (this.clearMethod != null)
                {
                    this.clearMethod();
                }
                else if (this.removeMethod != null)
                {
                    var snapshot = Enumerable.ToList(this);
                    foreach (T o in snapshot)
                    {
                        this.Remove(o);
                    }
                }
                else
                {
                    var message = String.Format(
                                            CultureInfo.CurrentCulture,
                                            Strings.SequenceNotRefillable,
                                            "Clear",
                                            this.sequence.GetType().FullName);
                    throw new NotSupportedException(message);
                }
            }

            public bool Contains(T item)
            {
                foreach (T obj in this)
                {
                    if (obj.Equals(item))
                    {
                        return true;
                    }
                }
                return false;
            }

            public void CopyTo(T[] array, int arrayIndex)
            {
                CollectionServices.CopyTo(this, item => item, array, arrayIndex);
            }

            public int Count
            {
                get
                {
                    // Enumerable.Count calls ICollection<>.Count, so we must avoid it
                    int count = 0;

                    IEnumerator en = this.GetEnumerator();
                    while (en.MoveNext())
                    {
                        count++;
                    }

                    return count;
                }
            }

            public bool IsReadOnly
            {
                get { return false; }
            }

            public bool Remove(T item)
            {
                if (this.removeMethod != null)
                {
                    return this.removeMethod(item);
                }
                else
                {
                    var message = String.Format(
                        CultureInfo.CurrentCulture,
                        Strings.SequenceNotRefillable,
                        "Remove",
                        this.sequence.GetType().FullName);
                    throw new NotSupportedException(message);
                }
            }

            IEnumerator IEnumerable.GetEnumerator()
            {
                return this.sequence.GetEnumerator();
            }

            public IEnumerator<T> GetEnumerator()
            {
                return this.sequence.Cast<T>().GetEnumerator();
            }
        }

        private class WeakCollectionImplementationINCC<T> : WeakCollectionImplementation<T>, INotifyCollectionChanged
        {
            private NotifyCollectionChangedHelper helper;

            public WeakCollectionImplementationINCC(
                IEnumerable sequence,
                MethodInfo addMethodInfo,
                MethodInfo clearMethodInfo,
                MethodInfo removeMethodInfo)
                : base(sequence, addMethodInfo, clearMethodInfo, removeMethodInfo)
            {
                this.helper = new NotifyCollectionChangedHelper(this, sequence as INotifyCollectionChanged);
            }

            event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
            {
                add { this.helper.CollectionChanged += value; }
                remove { this.helper.CollectionChanged -= value; }
            }
        }
    }
}